4.3 编译器的优化与调试

编译器优化在之前的章节我们有提到过,当时我们讲解了局部优化。

本节我们将进行更多的讲解,了解编译器的优化也可以帮助我们提高开发效率,在开发中即解决一些使用问题。

本节代码存放目录为 lesson11

编译器优化技术

编译器优化是指编译器在生成最终的机器代码之前,对代码进行的一系列改进,以提高程序的执行效率或减少内存占用。下面我们将展示几种优化方式以帮助我们思考。

内联展开

内联展开是指将函数调用替换为函数体本身,从而避免函数调用的开销。Go编译器会自动决定是否进行内联展开,通常是针对那些短小、频繁调用的函数。

func add(a, b int) int {
    return a + b
}

func main() {
    result := add(2, 3)  // 编译器可能会将此调用内联展开
    fmt.Println(result)
}

优化后的可能情况:

resultO := 2 + 3
fmt.Println(resultO)

还有可能进一步优化为:
fmt.Println(5)

在上面的代码中,直接将函数调用替换为了2+3,这样肯定是更简单直接的。


循环优化

循环优化包括多种技术,如循环展开、循环合并、循环分割和循环交换等。

  • 循环展开:通过展开循环体,减少循环的迭代次数,从而降低循环控制的开销。

  • 循环合并:将多个独立的循环合并为一个,以减少循环控制的开销。

  • 循环分割:将一个复杂的循环拆分为多个简单的循环,提高并行化的可能性。

  • 循环交换:调整循环嵌套的顺序,以提高缓存命中率或并行化。

示例如下所示:

for i := 0; i < 100; i++ {
    // 原始代码
}

编译器可能会将其优化为:

for i := 0; i < 100; i += 2 {
    // 展开后的循环
    // 第1次迭代
    // 第2次迭代
}

死代码消除

编译器会删除那些永远不会执行的代码或对程序结果无影响的代码,从而减少不必要的指令和数据。

示例如下所示:

func main() {
    var x = 10
    if false {
        fmt.Println("This code will never run")
    }
}

编译器会识别并删除if false条件下的代码块,因为它永远不会执行。


常量折叠和传播

编译器会在编译时计算表达式的常量值,并将它们直接替换到代码中,从而减少运行时的计算。

示例如下所示:

const a = 3
const b = 4
const c = a * b // 编译器会将c的值直接替换为12

编译参数与调优

Go编译器提供了一些编译参数,可以帮助开发者控制编译行为,从而进行性能调优或调试。

  • -gcflags:可以用来传递编译器的优化标志。例如,go build -gcflags="-N -l"会禁用优化和内联展开,用于调试。

  • -ldflags:用于传递给链接器的标志,可以控制最终可执行文件的大小和行为。

  • -race:用于检测数据竞争问题,在并发代码中非常有用。

示例如下所示:

go build -gcflags="-N -l" -o example lesson12.go

此命令禁用了优化和内联展开,用于生成更易于调试的代码。

我们来测试一下,首先我们允许优化进行编译,我们查看汇编代码:

go tool objdump -s main example

在输出中我们会找到下面的指令:

MOVD $5, R0

上面的指令就直接将5加载到寄存器,之后进行输出操作。

那么我么再禁用优化后观察一下:

go build -gcflags="-N -l" -o example lesson12.go

我们再次查看汇编代码,会发现之前很多没有的东西都展示出来了,这就说明了这是没有经过优化的。

我们可以看到这样的输出:

MOVD $5, R3
MOVD R3, 40(RSP)

这其实执行的就是:

resultO := 2 + 3

调试编译器行为

调试编译器行为有助于理解编译器如何优化代码,并帮助我们识别潜在的性能问题或不必要的开销。

我们在之前的章节中提到过pprof,这是比较方便的工具,使用也比较简单,可以对照官方示例尝试使用。

另外我们可以通过查看编译器的输出了解更多内容。

  • go tool compile -S:生成汇编代码,帮助我们了解编译器的优化决策。

    例如:go tool compile -S lesson12.go

  • go tool objdump:反汇编工具,用于分析可执行文件的汇编代码。这我们在上文也讲到过,主要就是用于查看到编译后的汇编代码。

我们还可以使用go build -gcflags="-m"可以让编译器输出优化决策信息,例如是否进行了内联展开,是否消除了某些代码等。

如下所示:

go build -gcflags="-m" lesson12.go

结果输出如下:

# command-line-arguments
./lesson12.go:11:6: can inline add
./lesson12.go:16:15: inlining call to add
./lesson12.go:17:13: inlining call to fmt.Println
./lesson12.go:20:13: inlining call to fmt.Println
./lesson12.go:22:13: inlining call to fmt.Println
./lesson12.go:17:13: ... argument does not escape
./lesson12.go:17:13: result escapes to heap
./lesson12.go:20:13: ... argument does not escape
./lesson12.go:20:13: resultO escapes to heap
./lesson12.go:22:13: ... argument does not escape
./lesson12.go:22:14: c escapes to heap

执行命令后编译器为我们输出了优化决策信息,例如./lesson12.go:11:6: can inline add告诉我们add函数可以被内联处理;c escapes to heap告诉我们c被分配到了堆内存上。

小结

理解编译的优化其实主要是帮助我们了解官方的优化逻辑,这样我们在实际开发的时候就可以按照这种风格去进行,那么我们的程序性能也就可以得到一定的保证。

另外还有一些关于输出包体大小的内容,本书不打算进行讲解。我们认为Go语言的性能以及它所提供的便利,足以弥补了编译包体大小的问题。

results matching ""

    No results matching ""